Skip to content

feat(plugin-terminals): terminals plugin with readonly + interactive PTY modes#37

Merged
antfu merged 11 commits into
devframes:mainfrom
antfubot:plugin-terminal-2
Jun 23, 2026
Merged

feat(plugin-terminals): terminals plugin with readonly + interactive PTY modes#37
antfu merged 11 commits into
devframes:mainfrom
antfubot:plugin-terminal-2

Conversation

@antfubot

Copy link
Copy Markdown
Collaborator

Summary

Adds @devframes/plugin-terminals — the built-in terminals plugin (#2 in the plugin planning index) — as a new plugins/* workspace. It's a portable, hub-native terminal panel built on the core devframe RPC + streaming surface (no hard hub dependency): it runs standalone via the CLI, mounts into a Vite host, and docks inside a hub.

Modes

  • Readonly — a piped child process whose merged stdout/stderr is streamed to viewers; write is rejected (DP_TERMINALS_0003). Ideal for dev servers / logs.
  • Interactive — a real PTY (@homebridge/node-pty-prebuilt-multiarch, prebuilt so there's no native compile step) that accepts keystrokes and resize. Degrades to a piped child process with a diagnostic when no PTY backend is available.

TUI support (e.g. Claude Code)

Interactive sessions are backed by a genuine pseudo-terminal, verified end-to-end:

  • the child sees a real TTY (process.stdout.isTTY === true),
  • stdin is forwarded keystroke-by-keystroke,
  • resize propagates to the PTY and fires SIGWINCH (so TUIs re-layout).

What's included (plugins/terminals/)

  • Node: TerminalManager (PTY + pipe backends; one stream per session kept open for the session's life so restart reuses the same id), setupTerminals(ctx), allow-list spawn model (presets + opt-in allowArbitraryCommands, default-deny), structured DP_TERMINALS_* diagnostics.
  • RPC (defineRpcFunction, namespaced devframes-plugin-terminals:*): list, presets, spawn, write, resize, terminate, restart, remove, with declare module 'devframe' augmentation for server functions + shared-state keys.
  • Client: mountTerminals() xterm.js renderer (CSS inlined, self-contained) — input wired for interactive, disabled for readonly, fit/resize handling. Reusable by the SPA and as a hub custom-render entry.
  • SPA: vanilla TS + Vite, served by the CLI and mountable as an iframe dock in a hub.
  • Adapters: . (createTerminalsDevframe factory + default export), /cli, /vite, plus bin.mjs. Mount path follows resolveBasePath (/ standalone, /__id/ hosted).

Repo wiring

pnpm-workspace.yaml (plugins/* glob + catalogs for node-pty & xterm + allow-build), turbo.json, alias.ts / tsconfig.base.json, vitest.config.ts.

Verification

pnpm lint && pnpm test && pnpm build all green (379 tests, 38 files). Plugin suite covers readonly streaming + exit, readonly write rejection, interactive PTY stdin, TTY detection, SIGWINCH resize, restart id reuse, list/remove, presets, and arbitrary-command rejection — over a real HTTP + WebSocket harness. tsnapi API snapshots added for every entry.

This PR was created with the help of an agent.

@netlify

netlify Bot commented Jun 18, 2026

Copy link
Copy Markdown

Deploy Preview for devfra ready!

Name Link
🔨 Latest commit 3afc3e7
🔍 Latest deploy log https://app.netlify.com/projects/devfra/deploys/6a3a1b290821440008af26b0
😎 Deploy Preview https://deploy-preview-37--devfra.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

antfubot added 7 commits June 19, 2026 14:01
…ive PTY modes

Introduce @devframes/plugin-terminals, a portable hub-native terminal
panel built on the core devframe RPC + streaming surface (no hard hub
dependency). It runs standalone via the CLI, mounts into a Vite host,
and docks inside a hub.

Two interaction modes:
- readonly: a piped child process whose merged output is streamed to
  viewers; input is rejected. Ideal for dev servers / logs.
- interactive: a real PTY (node-pty prebuilt) that accepts keystrokes
  and resize, so full-screen TUIs (vim, htop, Claude Code) render
  correctly. Falls back to a piped child process with a diagnostic when
  no PTY backend is present.

Output streams over a per-session channel (one stream kept open for the
session's life so restart reuses the same id), session metadata syncs via
shared state, and spawning is allow-list gated (presets + opt-in
arbitrary commands). Ships node + client + cli + vite entries plus a
self-contained xterm SPA, structured DP_TERMINALS diagnostics, and an
e2e test suite covering streaming, stdin, TTY detection, and SIGWINCH.
…ith rename

- Follow the system color mode and react to changes at runtime: the UI
  chrome (CSS variables) and every xterm instance switch between dark and
  a GitHub-light palette without reload, driven by prefers-color-scheme.
- Tab labels show the live foreground process of the controlling TTY
  (e.g. bash → vim → bash), polled from node-pty for PTY sessions.
- Custom renaming: double-click a tab to edit inline; backed by a new
  `devframes-plugin-terminals:rename` RPC. Display precedence is
  customTitle > processName > base title.

Adds processName/customTitle to the session descriptor, a getProcessName
hook on the PTY backend, an unref'd poll timer (cleared on exit/restart/
remove/dispose), and tests for process-name tracking and renaming.
…etadata, per-package typecheck)

Merge upstream/main and reconcile the plugin with its newer baseline:
- resolve to nostics ^1.1.4 via the catalog and regenerate the lockfile
  (fixes the broken merged lockfile that failed frozen CI installs).
- supply the now-required DevframeDefinition metadata (version,
  packageName, homepage, description) from package.json.
- add a `typecheck` script and switch tsconfig to explicit include/exclude
  (drop composite) so `turbo run typecheck` passes for the package.
- refresh tsnapi snapshots for the nostics v1 diagnostics handle shape.
- Gate the PTY-semantics tests (stdin echo, SIGWINCH resize, foreground
  process name) to POSIX; Windows keeps the isTTY interactive coverage.
  These rely on behaviours conpty doesn't provide (no SIGWINCH; `.process`
  returns the TERM name).
- Ignore the Windows `xterm-256color` TERM-name fallback so it never
  surfaces as a session label.
- Dispose the terminal manager on test server close so spawned PTY/piped
  child processes don't leak across runs.
…, + tab

- Mirror the active terminal into the URL hash (`#id=<sessionId>`) and react
  to external hash changes (links, back/forward, manual edits).
- Spawn and select a fresh interactive session on every page load.
- Move the "new terminal" affordance to a compact "+" pinned at the end of
  the tab strip.

Avoids refocusing the active terminal on background shared-state updates by
only fitting/focusing when the selection actually changes.
The prebuilt PTY binary isn't published for every Node ABI on every OS
(e.g. Node 26 on Windows 404s and the source-build fallback crashes),
which failed `pnpm install`. node-pty is already lazily imported with a
piped-child fallback, so move it to optionalDependencies — a missing
prebuild is now non-fatal.

Gate the PTY tests on actual backend availability: the real-TTY check runs
wherever a PTY exists (incl. Windows conpty), while the POSIX-only
semantics (SIGWINCH, foreground process name, stdin echo) stay POSIX-gated.
…d of spawning

A reload was always starting a new shell: the sessions shared state
resolves with its empty initial value and backfills the server's sessions
asynchronously, so the autostart check always saw an empty list.

Decide autostart from the authoritative `list` RPC and seed the initial
render from it, so a refresh restores the persisted sessions (reselecting
the URL-hashed one) and only spawns a shell when none exist.
@antfubot antfubot force-pushed the plugin-terminal-2 branch from c58b6d9 to cfe10d0 Compare June 22, 2026 00:38
antfubot added 4 commits June 22, 2026 01:34
Restyle the terminal panel after the antfu / vitejs-devtools (rolldown)
aesthetic:

- Adopt presetWind4 with semantic tokens (bg-base, bg-secondary,
  border-base, color-active, op-fade, op-mute) and a green primary accent;
  light and dark are designed together and driven by the `.dark` class.
- Replace ad-hoc inline color classes with the token system and reusable
  shortcuts (btn-action, btn-icon, tab-item, status dots, mode badges).
- Cleaner shell: brand + session tabs with status dots and hover-close, a
  "+" icon button, a presets dropdown, and a details toolbar with restart /
  kill icon actions. Phosphor duotone icons throughout.
- Add a global stylesheet (base shell, themed scrollbars, xterm host) and
  tie the xterm themes to the app surface (off-black #111, primary cursor).
@antfu antfu marked this pull request as ready for review June 23, 2026 06:48
Copilot AI review requested due to automatic review settings June 23, 2026 06:48
@antfu antfu merged commit 49bc3bd into devframes:main Jun 23, 2026
12 checks passed

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new built-in workspace plugin, @devframes/plugin-terminals, implementing a portable terminals panel that can run standalone (CLI/SPA) or mount into a Vite host via the existing devframe RPC + streaming surface.

Changes:

  • Introduces plugins/terminals workspace: node-side terminal manager (PTY + piped fallback), RPC surface, client renderer, and SPA.
  • Wires the new workspace into repo build/test/type infrastructure (tsconfig paths, turbo pipeline, vitest projects, pnpm catalogs/lockfile).
  • Adds tsnapi public API snapshots for the new package entrypoints.

Reviewed changes

Copilot reviewed 49 out of 59 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
vitest.config.ts Registers plugins/terminals as a vitest workspace project.
turbo.json Adds Turbo build pipeline entry for @devframes/plugin-terminals.
tsconfig.base.json Adds TS path aliases for @devframes/plugin-terminals/* entrypoints.
pnpm-workspace.yaml Adds catalogs and allowBuilds entry for the optional PTY backend + frontend deps for the plugin SPA/client.
pnpm-lock.yaml Locks new dependency graph for the terminals plugin workspace.
alias.ts Adds runtime/build alias entries for the new plugin entrypoints.
plugins/terminals/uno.config.ts UnoCSS theme + shortcuts for the terminals SPA/client UI.
plugins/terminals/tsdown.config.ts tsdown build configuration for node/neutral builds and combined DTS emission.
plugins/terminals/tsconfig.json Plugin-local TS config (DOM lib enabled; includes tests/build config).
plugins/terminals/test/terminals.test.ts End-to-end RPC + streaming tests for readonly + interactive (PTY) behavior.
plugins/terminals/test/_utils.ts HTTP+WS harness to boot the devframe for tests and collect streaming output.
plugins/terminals/src/vite.ts Vite adapter helper (terminalsVite) built on viteDevBridge.
plugins/terminals/src/types.ts Public types for sessions, presets, spawn requests, and options.
plugins/terminals/src/spa/vite.config.ts Vite config for building the bundled SPA (base: './').
plugins/terminals/src/spa/main.ts SPA entry mounting the terminals client.
plugins/terminals/src/spa/index.html SPA HTML shell.
plugins/terminals/src/rpc/schemas.ts Valibot schemas for terminal RPC request/response payloads.
plugins/terminals/src/rpc/index.ts RPC function aggregation + devframe module augmentation for server fns/shared-state keys.
plugins/terminals/src/rpc/functions/write.ts RPC action to write input to a session.
plugins/terminals/src/rpc/functions/terminate.ts RPC action to terminate a session’s process.
plugins/terminals/src/rpc/functions/spawn.ts RPC action to spawn a new session.
plugins/terminals/src/rpc/functions/restart.ts RPC action to restart a session in-place.
plugins/terminals/src/rpc/functions/resize.ts RPC action to resize a session.
plugins/terminals/src/rpc/functions/rename.ts RPC action to set/clear a session’s custom title.
plugins/terminals/src/rpc/functions/remove.ts RPC action to remove a session and its resources.
plugins/terminals/src/rpc/functions/presets.ts RPC query to list spawnable presets.
plugins/terminals/src/rpc/functions/list.ts RPC query to list session descriptors.
plugins/terminals/src/node/manager.ts Core node-side session lifecycle manager + streaming + shared-state publishing.
plugins/terminals/src/node/index.ts Node entrypoint: wires manager + registers RPC functions.
plugins/terminals/src/node/diagnostics.ts Plugin diagnostics (DP_TERMINALS_000x) via nostics.
plugins/terminals/src/node/context.ts Context-scoped manager storage (WeakMap).
plugins/terminals/src/node/backend.ts PTY backend loader + pipe fallback implementation.
plugins/terminals/src/index.ts Devframe definition factory + default export; CLI config + SPA dist wiring.
plugins/terminals/src/env.d.ts Svelte/UnoCSS/CSS module typings for TS.
plugins/terminals/src/constants.ts Shared constants (IDs, state keys, defaults).
plugins/terminals/src/client/vite.config.ts Vite build for embeddable client renderer bundle.
plugins/terminals/src/client/TerminalView.svelte Xterm-based terminal view + input/resize + stream subscription.
plugins/terminals/src/client/styles.css Base styles and xterm viewport styling.
plugins/terminals/src/client/index.ts Public mountTerminals() API + exports for consumers.
plugins/terminals/src/client/App.svelte Terminals UI: session tabs, presets menu, toolbar, view switching.
plugins/terminals/src/cli.ts CLI adapter factory createTerminalsCli().
plugins/terminals/package.json New workspace package manifest + exports + build scripts + deps.
plugins/terminals/bin.mjs Package bin entrypoint for the terminals CLI.
tests/snapshots/tsnapi/@devframes/plugin-terminals/vite.snapshot.js tsnapi JS API snapshot for /vite.
tests/snapshots/tsnapi/@devframes/plugin-terminals/vite.snapshot.d.ts tsnapi DTS API snapshot for /vite.
tests/snapshots/tsnapi/@devframes/plugin-terminals/types.snapshot.js tsnapi JS API snapshot for /types.
tests/snapshots/tsnapi/@devframes/plugin-terminals/types.snapshot.d.ts tsnapi DTS API snapshot for /types.
tests/snapshots/tsnapi/@devframes/plugin-terminals/rpc.snapshot.js tsnapi JS API snapshot for /rpc.
tests/snapshots/tsnapi/@devframes/plugin-terminals/rpc.snapshot.d.ts tsnapi DTS API snapshot for /rpc.
tests/snapshots/tsnapi/@devframes/plugin-terminals/node.snapshot.js tsnapi JS API snapshot for /node.
tests/snapshots/tsnapi/@devframes/plugin-terminals/node.snapshot.d.ts tsnapi DTS API snapshot for /node.
tests/snapshots/tsnapi/@devframes/plugin-terminals/index.snapshot.js tsnapi JS API snapshot for package root.
tests/snapshots/tsnapi/@devframes/plugin-terminals/index.snapshot.d.ts tsnapi DTS API snapshot for package root.
tests/snapshots/tsnapi/@devframes/plugin-terminals/constants.snapshot.js tsnapi JS API snapshot for /constants.
tests/snapshots/tsnapi/@devframes/plugin-terminals/constants.snapshot.d.ts tsnapi DTS API snapshot for /constants.
tests/snapshots/tsnapi/@devframes/plugin-terminals/client.snapshot.js tsnapi JS API snapshot for /client.
tests/snapshots/tsnapi/@devframes/plugin-terminals/client.snapshot.d.ts tsnapi DTS API snapshot for /client.
tests/snapshots/tsnapi/@devframes/plugin-terminals/cli.snapshot.js tsnapi JS API snapshot for /cli.
tests/snapshots/tsnapi/@devframes/plugin-terminals/cli.snapshot.d.ts tsnapi DTS API snapshot for /cli.
Files not reviewed (1)
  • pnpm-lock.yaml: Generated file

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +12 to +13
cols: v.optional(v.number()),
rows: v.optional(v.number()),
'types': 'src/types.ts',
}

// Three configs:
Comment on lines +174 to +191
<button
type="button"
class="group tab-item {activeId === s.id ? 'tab-item-active' : ''}"
title={`${displayName(s)} — double-click to rename`}
onclick={() => (activeId = s.id)}
ondblclick={(e) => { e.preventDefault(); e.stopPropagation(); renamingId = s.id }}
>
<span class="h-1.5 w-1.5 rounded-full shrink-0 {statusDot(s.status)}"></span>
<span class="truncate">{displayName(s)}</span>
<span
role="button"
tabindex="-1"
aria-label="Close terminal"
class="i-ph-x op0 group-hover:op60 hover:op100! transition-opacity shrink-0"
onclick={(e) => { e.stopPropagation(); rpc.call('devframes-plugin-terminals:remove', { id: s.id }).catch(() => {}) }}
onkeydown={() => {}}
></span>
</button>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants